﻿using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using PMS.Core.Utils.Authentication;
using PMS.Core.Utils.Http;
using PMS.Data.Entities.Customers;
using PMS.Data.Entities.Settings;
using PMS.Services.EntityServices.Customers;
using PMS.Services.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace PMS.Services.Authentication
{
    /// <summary>
    /// Represents service using jwt middleware for the authentication
    /// </summary>
    public partial class JwtAuthenticationService : IAuthenticationService
    {
        #region Fields

        private readonly SecuritySettings _securitySettings;
        private readonly IWebHelper _webHelper;
        private readonly ICustomerService _customerService;
        private readonly IHttpContextAccessor _httpContextAccessor;

        private Customer _cachedCustomer;

        #endregion

        #region Ctor

        public JwtAuthenticationService(ICustomerService customerService,
            IWebHelper webHelper,
            SecuritySettings securitySettings,            
            IHttpContextAccessor httpContextAccessor)
        {
            _securitySettings = securitySettings;
            _webHelper = webHelper;
            _customerService = customerService;
            _httpContextAccessor = httpContextAccessor;
        }

        #endregion

        #region Utils
       
        protected string JwtSignIn(Customer customer)
        {
            var validIssuer = string.IsNullOrEmpty(_securitySettings.JwtValidIssuer)
                            ? AuthenticationDefaultConfigs.JwtValidIssuer
                            : _securitySettings.JwtValidIssuer;
            var validAudience = string.IsNullOrEmpty(_securitySettings.JwtValidAudience)
                              ? AuthenticationDefaultConfigs.JwtValidAudience
                              : _securitySettings.JwtValidAudience;
            var securityKey = string.IsNullOrEmpty(_securitySettings.JwtIssuerSigningKey)
                            ? AuthenticationDefaultConfigs.JwtIssuerSigningKey
                            : _securitySettings.JwtIssuerSigningKey;
            var expireTime = _securitySettings.JwtExpirationMinutes <= 0
                           ? AuthenticationDefaultConfigs.JwtExpirationMinutes
                           : _securitySettings.JwtExpirationMinutes;

            var claims = new List<Claim>();
            if (customer.CustomerGuid != Guid.Empty)
                claims.Add(new Claim(ClaimTypes.NameIdentifier, customer.CustomerGuid.ToString(), ClaimValueTypes.String, validIssuer));
            if (!string.IsNullOrEmpty(customer.Mobile))
                claims.Add(new Claim(ClaimTypes.MobilePhone, customer.Mobile, ClaimValueTypes.String, validIssuer));
            DateTime authTime = DateTime.UtcNow;
            DateTime expiresAt = authTime.AddMinutes(Convert.ToDouble(expireTime));
            claims.Add(new Claim(ClaimTypes.Expiration, expiresAt.ToString(), ClaimValueTypes.DateTime, validIssuer));
            //create principal for the current authentication scheme
            var userIdentity = new ClaimsIdentity(claims, AuthenticationDefaultConfigs.JwtAuthenticationScheme);

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            string tokenSignAlgorithm = SecurityAlgorithms.HmacSha256;
            //if (!string.IsNullOrEmpty(_customerSettings.HashedPasswordFormat))
            //{
            //    Type saType = typeof(SecurityAlgorithms);
            //    var fields = saType.GetFields();
            //    var field = fields.FirstOrDefault(p => p.GetValue(null).ToString().Equals(_customerSettings.HashedPasswordFormat, StringComparison.InvariantCultureIgnoreCase));
            //    if (field != null) tokenSignAlgorithm = field.GetValue(null).ToString();
            //}
            #region Another_way_of_signin_using_SigninAsync_but_notrecommend
            //也可使用公用方法登录，但不推荐,因为已经通过SetCookie将新生成登录凭证Jwt保存
            //更符合Jwt Serverless特性
            ////sign in
            //var userPrincipal = new ClaimsPrincipal(userIdentity);
            ////set value indicating whether session is persisted and the time at which the authentication was issued
            //var authenticationProperties = new AuthenticationProperties
            //{
            //    IsPersistent = isPersistent,
            //    IssuedUtc = authTime,
            //    ExpiresUtc = expiresAt
            //};
            //await _httpContextAccessor.HttpContext.SignInAsync(AuthenticationDefaultConfigs.JwtAuthenticationScheme, userPrincipal, authenticationProperties);
            #endregion
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = userIdentity, //new ClaimsIdentity(claims),//创建声明信息
                Issuer = validIssuer,                //Jwt token 的签发者
                Audience = validAudience,            //Jwt token 的接收者
                Expires = expiresAt,                 //过期时间
                IssuedAt = authTime,
                SigningCredentials = new SigningCredentials(key, tokenSignAlgorithm)//创建 token
            };
            var jwtToken = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
            return tokenHandler.WriteToken(jwtToken);
        }

        protected virtual string GetCustomerClientCookie()
        {
            var cookieName = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.CustomerClienCookie}";
            return _httpContextAccessor.HttpContext?.Request?.Cookies[cookieName];
        }

        /// <summary>
        /// Set cliient jwt cookie
        /// </summary>
        /// <param name="jwtToken">jwtToken</param>
        protected virtual void SetCustomerClientCookie(string jwtToken = null)
        {
            if (_httpContextAccessor.HttpContext?.Response == null)
                return;

            //delete current cookie value
            var cookieName = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.CustomerClienCookie}";
            _httpContextAccessor.HttpContext.Response.Cookies.Delete(cookieName);
            if (string.IsNullOrEmpty(jwtToken))
            {
                //cookieExpiresDate = DateTime.Now.AddMonths(-1);
                //jwtToken = string.Empty; //Cannot set null to cookie
                return;
            }
            //get date of cookie expiration
            var cookieExpires = CookieDefaultConfigs.CustomerCookieExpires;
            var cookieExpiresDate = DateTime.Now.AddHours(cookieExpires);

            //set new cookie value
            var options = new CookieOptions
            {
                HttpOnly = false,
                Expires = cookieExpiresDate,
                Secure = _webHelper.IsCurrentConnectionSecured()
            };

            _httpContextAccessor.HttpContext.Response.Cookies.Append(cookieName, jwtToken, options);
        }
        #endregion

        #region Methods

        /// <summary>
        /// Sign in
        /// </summary>
        /// <param name="customer">Customer</param>
        /// <param name="isPersistent">Whether the authentication session is persisted across multiple requests</param>
        public virtual void SignIn(Customer customer, bool isPersistent)
        {
            if (customer == null)
                throw new ArgumentNullException(nameof(customer));

            string resultToken = JwtSignIn(customer);
            //cache authenticated customer
            _cachedCustomer = customer;
            SetCustomerClientCookie(resultToken);
        }

        /// <summary>
        /// Sign out
        /// </summary>
        public virtual void SignOut()
        {
            //reset cached customer
            _cachedCustomer = null;
            SetCustomerClientCookie();
        }

        /// <summary>
        /// Get authenticated customer
        /// </summary>
        /// <returns>Customer</returns>
        public virtual Customer GetAuthenticatedCustomer()
        {
            ClaimsPrincipal userPrincipal = null;
            //whether there is a cached customer
            if (_cachedCustomer != null)
                return _cachedCustomer;
            else if(_httpContextAccessor.HttpContext.User != null)
            {
                //see AuthenticationMiddleware on AuthenticationStartup.UsePMSAuthentication
                //UseAuthentication Middleware authenticated logedin user already
                userPrincipal = _httpContextAccessor.HttpContext.User;
            }
            else
            {
                string schema = AuthenticationDefaultConfigs.JwtAuthenticationScheme;
                string clientJwt = string.Empty;
                if (_webHelper.IsAjaxRequest(_httpContextAccessor.HttpContext.Request))
                    clientJwt = _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Authorization].FirstOrDefault()?.Split(" ").Last();
                //clientJwt = _httpContextAccessor.HttpContext.GetTokenAsync(schema, "access_token").Result;
                else
                    clientJwt = GetCustomerClientCookie();
                if (!string.IsNullOrEmpty(clientJwt))
                {
                    var authenticateResult = _httpContextAccessor.HttpContext.AuthenticateAsync(schema).Result;
                    if (!authenticateResult.Succeeded)
                    {
                        if (authenticateResult.Failure != null && authenticateResult.Failure.GetType() == typeof(SecurityTokenExpiredException))
                            SetCustomerClientCookie();
                        return null;
                    }

                    userPrincipal = authenticateResult.Principal;
                }
            }

            if (userPrincipal == null) return null;
            string issuer = string.IsNullOrEmpty(_securitySettings.JwtValidIssuer)
                          ? AuthenticationDefaultConfigs.JwtValidIssuer
                          : _securitySettings.JwtValidIssuer;
            Customer customer = null;
            var usermobileClaim = userPrincipal.FindFirst(claim => claim.Type == ClaimTypes.MobilePhone
                                && claim.Issuer.Equals(issuer, StringComparison.InvariantCultureIgnoreCase));
            if (usermobileClaim != null)
                customer = _customerService.GetCustomerByMobile(usermobileClaim.Value);
            if (customer == null)
            {
                //try to get customer by Guid
                var guidClaim = userPrincipal.FindFirst(claim => claim.Type == ClaimTypes.NameIdentifier
                    && claim.Issuer.Equals(issuer, StringComparison.InvariantCultureIgnoreCase));
                if (guidClaim != null)
                    customer = _customerService.GetCustomerByGuid((Guid)TypeDescriptor.GetConverter(typeof(Guid)).ConvertFrom(guidClaim.Value));
            }

            #region ValidateJwtUsingJwtSecurityTokenHandler
            //手动验证JWT凭证合法性，但即然注册Jwt服务时已经设置过验证规则，则使用AuthenticatAsync即可
            //以下方法提供验证JWT的另一种形式
            //try
            //{
            //    string clientJwt = string.Empty;
            //    var securityKey = string.IsNullOrEmpty(_securitySettings.JwtIssuerSigningKey)
            //                    ? AuthenticationDefaultConfigs.JwtIssuerSigningKey
            //                    : _securitySettings.JwtIssuerSigningKey;
            //    if (_webHelper.IsAjaxRequest(_httpContextAccessor.HttpContext.Request))
            //        clientJwt = _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Authorization].FirstOrDefault()?.Split(" ").Last();
            //        //clientJwt = _httpContextAccessor.HttpContext.GetTokenAsync(schema, "access_token").Result;
            //    else
            //        clientJwt = GetCustomerClientCookie();

            //    if (!string.IsNullOrEmpty(clientJwt))
            //    {
            //        var tokenHandler = new JwtSecurityTokenHandler();
            //        SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            //        var resultPrinciple = tokenHandler.ValidateToken(clientJwt, new TokenValidationParameters
            //        {
            //            ValidateIssuerSigningKey = true,
            //            IssuerSigningKey = key,
            //            ValidateIssuer = true,
            //            ValidateAudience = false,
            //            ValidIssuer = issuer,
            //            // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
            //            ClockSkew = TimeSpan.Zero
            //        }, out _);

            //        var usermobileClaim = resultPrinciple.FindFirst(claim => claim.Type == ClaimTypes.MobilePhone
            //                            && claim.Issuer.Equals(issuer, StringComparison.InvariantCultureIgnoreCase));
            //        if (usermobileClaim != null)
            //            customer = _customerService.GetCustomerByMobile(usermobileClaim.Value);
            //        if (customer == null)
            //        {
            //            //try to get customer by Guid
            //            var guidClaim = resultPrinciple.FindFirst(claim => claim.Type == ClaimTypes.NameIdentifier
            //                && claim.Issuer.Equals(issuer, StringComparison.InvariantCultureIgnoreCase));
            //            if (guidClaim != null)
            //                customer = _customerService.GetCustomerByGuid((Guid)TypeDescriptor.GetConverter(typeof(Guid)).ConvertFrom(guidClaim.Value));
            //        }
            //    }
            //}
            //catch (Exception)
            //{
            //    if (ex is SecurityTokenExpiredException)
            //        SetCustomerClientCookie();
            //    return null;
            //}
            #endregion

            //whether the found customer is available
            if (customer == null || !customer.IsActive || customer.IsDeleted)
                return null;

            //cache authenticated customer
            _cachedCustomer = customer;

            return _cachedCustomer;
        }

        #endregion
    }

}
